defmodule Dagger.Mod.Function do
  @moduledoc false

  alias Dagger.Mod.Helper
  alias Dagger.Mod.Object

  @doc """
  Define a Dagger function from the function definition.
  """
  @spec define(Dagger.Client.t(), module(), Object.function_def()) :: Dagger.Function.t()
  def define(%Dagger.Client{} = dag, module, {name, fun_def}) when is_atom(module) do
    args = Keyword.fetch!(fun_def, :args)
    return = Keyword.fetch!(fun_def, :return)

    dag
    |> Dagger.Client.function(
      Helper.camelize(name),
      define_type(dag, Dagger.Client.type_def(dag), return)
    )
    |> maybe_with_description(Object.get_function_doc(module, name))
    |> with_args(args, dag)
  end

  defp maybe_with_description(function, nil), do: function
  defp maybe_with_description(function, doc), do: Dagger.Function.with_description(function, doc)

  defp with_args(fun, args, dag) do
    args
    |> Enum.reduce(fun, fn {name, arg_def}, fun ->
      type = Keyword.fetch!(arg_def, :type)

      type_def =
        dag
        |> define_type(Dagger.Client.type_def(dag), type)

      opts =
        arg_def
        |> Keyword.take([:doc, :default_path, :ignore])
        |> Enum.reject(fn {_, value} -> is_nil(value) end)
        |> Enum.map(&normalize_arg_option/1)

      fun
      |> Dagger.Function.with_arg(name, type_def, opts)
    end)
  end

  defp define_type(_dag, type_def, :integer) do
    type_def
    |> Dagger.TypeDef.with_kind(Dagger.TypeDefKind.integer_kind())
  end

  defp define_type(_dag, type_def, :float) do
    type_def
    |> Dagger.TypeDef.with_kind(Dagger.TypeDefKind.float_kind())
  end

  defp define_type(_dag, type_def, :boolean) do
    type_def
    |> Dagger.TypeDef.with_kind(Dagger.TypeDefKind.boolean_kind())
  end

  defp define_type(_dag, type_def, :string) do
    type_def
    |> Dagger.TypeDef.with_kind(Dagger.TypeDefKind.string_kind())
  end

  defp define_type(dag, type_def, {:list, type}) do
    type_def
    |> Dagger.TypeDef.with_list_of(define_type(dag, Dagger.Client.type_def(dag), type))
  end

  defp define_type(dag, type_def, {:optional, type}) do
    dag
    |> define_type(type_def, type)
    |> Dagger.TypeDef.with_optional(true)
  end

  defp define_type(_dag, type_def, module) do
    case Module.split(module) do
      ["Dagger", "Void"] ->
        type_def
        |> Dagger.TypeDef.with_kind(Dagger.TypeDefKind.void_kind())
        |> Dagger.TypeDef.with_optional(true)

      # A module that generated by codegen.
      ["Dagger", name] ->
        type_def
        |> Dagger.TypeDef.with_object(name)

      [name] ->
        type_def
        |> Dagger.TypeDef.with_object(name)
    end
  end

  defp normalize_arg_option({:doc, doc}), do: {:description, doc}
  defp normalize_arg_option(opt), do: opt
end
